<?php

namespace Pinenut\Model2;

use \Log;

/**
 * Механизм выборки
 * Служит для формирования запроса на получение данных,
 * а так же фабрика сущностей модели
 *
 * @method string|Finder withLang()|withLang(string $langId) - устанавливает/возвращает язык данных
 *
 * @author Kosh
 *        
 */
class Finder {
	const OP_EQ = '==';
	const OP_IN = 'in';
	const OP_ALL = 'all';
	const OP_NEQ = '!=';
	const OP_NIN = 'nin';
	const OP_GTE = 'gte';
	const OP_GT = 'gt';
	const OP_LTE = 'lte';
	const OP_LT = 'lt';
	const ORDER_ASC = 1;
	const ORDER_DESC = - 1;
	
	/**
	 *
	 * @var \Pinenut\Model2
	 */
	protected $class;
	
	protected $where = array ();
	protected $fields = array ();
	protected $order = array ();
	protected $limit = 1000;
	protected $offset = 0;
	protected $_fieldNamePrefix = ''; // поле в сложной структуре документа,
	                                  // в котором будут происходить все обновления
	                                  // добавляется ко всем именам полей в условии
	protected $with;
	protected $active = false;
	
	/**
	 * Создаём новый поисковый запрос,
	 * передавая или имя сущности, или экземпляр
	 *
	 * @param string|\Pinenut\Model2 $class        	
	 */
	function __construct($class = null) {
		$this->setModel ( $class );
	}
	
	function setModel($class) {
		$this->class = $class;
	}
	
	function getModel() {
		return $this->class;
	}
	
	protected function andWhere($field, $op, $value) {
		$this->where [$field][$op] = array (
				'op' => $op,
				'field' => $field,
				'value' => $value 
		);
	}
	
	/**
	 * Добавляем условие с отрицанием
	 * 
	 * @param string $field        	
	 * @param mixed $value        	
	 * @return \Pinenut\Model2\Finder|\Pinenut\Model2\Updater
	 */
	function whereNot($field, $value) {
		$op = static::OP_NEQ;
		if (is_array ( $value )) {
			$op = static::OP_NIN;
		}
		
		$this->andWhere ( $field, $op, $value );
		
		return $this;
	}
	
	/**
	 * Добавляем условие
	 * 
	 * @param string $field        	
	 * @param mixed $value        	
	 * @param string $op        	
	 * @return \Pinenut\Model2\Finder|\Pinenut\Model2\Updater
	 */
	function where($field, $value, $op = null) {
		if (is_array ( $value ) && null == $op) {
			$op = static::OP_IN;
		}
		if (null === $op)
			$op = static::OP_EQ;
		
		$this->andWhere ( $field, $op, $value );
		
		return $this;
	}
	
	/**
	 *
	 * @param string $field        	
	 * @param mixed $value        	
	 * @return \Pinenut\Model2\Finder|\Pinenut\Model2\Updater
	 */
	function whereGreatOrEqual($field, $value) {
		$this->where ( $field, $value, static::OP_GTE );
		return $this;
	}
	
	/**
	 *
	 * @param boolean $enable        	
	 * @return \Pinenut\Model2\Finder
	 */
	function active($enable = true) {
		$this->active = $enable;
		return $this;
	}
	
	/**
	 *
	 * @param string $field        	
	 * @return \Pinenut\Model2\Finder
	 */
	function order($field) {
		$this->order = [ 
				$field => static::ORDER_ASC 
		];
		return $this;
	}
	
	/**
	 *
	 * @param string $field        	
	 * @return \Pinenut\Model2\Finder
	 */
	function orderReverse($field) {
		$this->order = [ 
				$field => static::ORDER_DESC 
		];
		return $this;
	}
	
	/**
	 *
	 * @param string $field        	
	 * @return \Pinenut\Model2\Finder
	 */
	function andOrder($field) {
		$this->order [$field] = static::ORDER_ASC;
		return $this;
	}
	
	/**
	 *
	 * @param string $field        	
	 * @return \Pinenut\Model2\Finder
	 */
	function andOrderReverse($field) {
		$this->order [$field] = static::ORDER_DESC;
		return $this;
	}
	
	/**
	 *
	 * @param int $value        	
	 * @return \Pinenut\Model2\Finder
	 */
	function offset($value = 0) {
		$this->offset = $value;
		return $this;
	}
	
	/**
	 *
	 * @param int $value        	
	 * @return \Pinenut\Model2\Finder
	 */
	function limit($value = 1000) {
		$this->limit = $value;
		return $this;
	}
	
	/**
	 *
	 * @param string[] $fields        	
	 * @return \Pinenut\Model2\Finder
	 */
	function fields(array $fields = array()) {
		$this->fields = $fields;
		return $this;
	}
	
	/**
	 *
	 * @return static|array|null
	 */
	function one() {
		$class = $this->class;
		$this->limit(1);
		$data = $class::fetch($this, $this->active);
		if (empty ( $data ))
			return null;
		
		return reset ( $data );
	}
	function count() {
		$class = $this->class;
		return $class::fetch($this, false, true);		
	}
	function all() {
		$class = $this->class;
		return $class::fetch($this, $this->active);
	}
	
	/**
	 * Возвращаем массив ключ-значение для выборки,
	 * где ключ - ключ базы, значение - выбранное поле
	 *
	 * TODO: сделать доступ к вложенным значениям
	 *
	 * @return array
	 */
	function column($column, callable $filter = null) {
		$class = $this->class;
		$this->fields([$column]);
		
		$raws = $class::fetch($this, false);
		
		if (empty ( $raws ))
			return null;
		
		$array = [ ];
		if (is_callable ( $filter )) {
			foreach ( $raws as $raw ) {
				$array [( string ) $raw ['_id']] = $filter($raw [$column]);
			}
		} else {
			foreach ( $raws as $raw ) {
				$array [( string ) $raw ['_id']] = $raw [$column];
			}
		}
		
		return $array;
	}
	
	function issetWhere($field) {
		return ! empty ( $this->where [$field] );
	}
	
	/**
	 * Возвращает хэш условия запроса
	 * Нужно для группировки обновлений
	 * ...
	 * ну и мало ли
	 * 
	 * @return string
	 */
	function hash() {
		return md5 ( $this->_fieldNamePrefix . serialize ( $this->where ) );
	}
	function prependFieldNames($part) {
		$this->_fieldNamePrefix = $part . (! empty ( $this->_fieldNamePrefix ) ? '.' . $this->_fieldNamePrefix : '');
	}
	
	/**
	 * Устанавливаем корректные имена полей, добавляя префикс
	 */
	function applyFieldNames() {
		$wheres = array ();
		foreach ( $this->where as $ops ) {
			foreach ($ops as &$where) {
				$field = (! empty ( $this->_fieldNamePrefix ) ? $this->_fieldNamePrefix . '.' : '') . $where ['field'];
				// TODO: удаляем лишнее, надо перенести в монговский адаптер
				$field = preg_replace ( '#(\.\$)#', '', $field );
				$where ['field'] = $field;
			}
			$wheres [$field] = $ops;
		}
		$this->where = $wheres;
		$this->_fieldNamePrefix = '';
	}
	
	function getWhere() {
		return $this->where;
	}
	function getFields() {
		return $this->fields;
	}
	function getOrder() {
		return $this->order;
	}
	function getLimit() {
		return $this->limit;
	}
	function getOffset() {
		return $this->offset;
	}

	public function setWhere($where){
		$this->where = $where;
	}

	public function setFields($fields){
		$this->fields = $fields;
	}

	public function setOrder($order){
		$this->order = $order;
	}

	public function __call($name, $arguments)
	{
		if (strpos($name, 'with') !== 0) {
			return;
			#throw new \BadMethodCallException("Method '$name' not found.");
		}

		if ($name == 'with') {
			return $this->with;
		}

		$key  = strtolower(str_replace('with', '', $name));

		if (!count($arguments)) {
			return $this->with[$key];
		}

		$this->with[$key] = count($arguments) > 1 ? $arguments : $arguments[0];
		return $this;
	}
}
